Webpack TreeShaking

서론

리액트 웹 앱을 개발 한다면 번들링에 주로 사용 되는 도구는 webpack일 것입니다. webpack의 주 역할은 js, css 등등 여러 모듈화 되어 여러 파일로 나누어진 소스 파일을 하나 또는 여러 파일로 합쳐 주어 배포에 용이하게 해줍니다.

이 과정 중에서 소스 코드들을 최적화 하여 번들(합쳐진 파일)의 용량을 작게 해주기도 합니다. 소스 코드의 양을 줄이는 최적화에는 크게 두가지가 있습니다. 코드 자체를 압축하는 방식(minify)와 소스 코드 중 사용하지 않는 코드를 버리는 (treeShaking) 두가지 방식이 있습니다.

Minify

minify는 내가 작성한 소스 코드가 더 간소화 되지만 동작은 동일하게 하도록 바꾸어 줍니다.

webpack에서는 기본적으로 production mode로 번들링하게 되면 terser 를 이용하여 압축합니다

어떻게 terser플러그인이 소스 코드를 최적화 하는지 아래 예시를 통해 확인 할 수 있습니다.

  • 최적화 전

    var x = {
        baz_: 0,
        foo_: 1,
        calc: function() {
            return this.foo_ + this.baz_;
        }
    };
    x.bar_ = 2;
    x["baz_"] = 3;
    console.log(x.calc());
  • 최적화 후

    var x={o:(0,3),t:1,i:function(){return this.t+this.o},s:2};console.log(x.i());

동작은 동일하게 하지만 소스 코드의 파일의 용량은 크게 줄었을 것입니다.

TreeShaking

treeShaking은 라이브러리의 소스 코드 중 내가 사용한 부분만 import 해주어 번들에 포함되고 나머지 사용하지 않는 코드는 버려주는 매우 훌륭한 마법 같은 기능입니다.

webpack을 소개 해주는 자료들에서 이 기능을 소개해주는 것을 보았을 때 정말 좋은 세상에서 개발하고 있구나 생각을 했습니다.

하지만..

직접 프로젝트의 번들링 최적화를 해보면서 역시나 세상은 만만치 않구나 라는 것을 알게 되었고 TreeShaking에는 많은 제약 조건이 있다는 것을 알게 되었습니다.

첫 번째 제약 조건 모듈 시스템

Treeshaking이 가능 하려면 소스코드가 ES modules로 빌드 되어있어야 합니다.

JS에 여러 모듈 시스템이 존재 하지만 주로 commonJS, ES modules 두가지 방식이 주를 이루고 있습니다.

  • commonJSconst lodash = require('lodash') 처럼 require 문을 사용해 모듈을 불러오고 nodeJS에서 지원하는 모듈 시스템 이기도 합니다.
  • ES modulesimport lodash from 'lodash' import 문을 사용해 모듈을 불러옵니다.

webpack에선 Treeshaking을 하기 위한 조건으로 ES modules로 빌드 된 소스 코드만 지원 합니다.

한마디로 내가 npm을 통해 어떤 라이브러리를 받았을때 앞서 말씀 드린 것처럼 해당 라이브러리가 ES module로 빌드 되어 있어야 treeShaking을 통해 최적화가 이루어집니다.

하지만 대다수의 npm 라이브러리들은 기본적으로 nodeJS에서 동작이 되게 배포 하므로 ES modules이 아닌 commonJS로 빌드 되어 배포 되어져 있어 아쉽게도 거의 Treeshaking을 통한 최적화는 이루어지지 않습니다.

그렇다고 방법이 없는 것은 아닙니다.

일부 용량이 큰 라이브러리의 경우 위와 같은 이유로 ES modules로 빌드 된 버전을 따로 배포 하기도 합니다.

lodash, lodash-es 둘 다 같은 라이브러리 이지만 빌드 된 모듈 시스템만 다르게 하여 배포 됩니다. webpack을 사용 한다면 최적화를 위해 lodash-es 를 받아서 사용 할 수도 있겠습니다.

하지만 주의 할 점은 ES modules로 빌드된 외부 라이브러리를 설치 할 경우 테스트 환경에서 오류가 나는 경우가 발생 할 수 있습니다. 테스트 런타임은 nodeJS이기 때문에 ES modules을 불러오지 못하기 때문입니다.

jest에는 babel-jest라는 플러그인이 내장 되어 있어 보통 테스트 환경 일 때 바벨 설정에서 commonJS로 트랜스파일되게 분기 처리를 합니다. 하지만 기본적으로 node_modules 폴더는 바벨 트랜스파일 대상에서 제외 대기 때문에 ES modules로 빌드된 외부 패키지들이 오류를 발생하게 합니다.

따라서 ES modules로 빌드된 외부 패키지들을 설치 하신후 테스트 환경을 구축하신다면 commonJS로 빌드되게 설정을 해주어야하는 번거로운 단점이 존재 합니다

두 번째 제약 조건 코드간의 의존성 sideEffects

package.jsonsideEffects속성이 false이어야 합니다.

sideEffects 옵션은 직접 내가 만든 패키지를 다른 개발 환경에서 트리쉐이킹이 되도록 최적화 하여 개발 하겠다면 도움이 되는 내용이지만 외부 라이브러리들을 최적화와는 무관한 내용이므로 직접 만드시는 상황이 아니면 넘어 가셔도 괜찮습니다.

내가 직접 프론트엔드에서 사용 할 공통 모듈을 개발하는데 treeShaking을 통해 최적화 되도록 한다면 위의 첫번째 제약 조건에 나온 것 처럼 ES modules로 빌드하여 배포하여야 합니다. 또 한가지 중요한 옵션인 sideEffects가 있습니다.

sideEffects 옵션은 webpack에서 설정하는 것이 아닌 배포되는 패키지의 package.json에 정의 해주는 옵션입니다. 기본 값은 true로 되어 있습니다. 이 옵션은 패키지 개발자가 webpack에게 사용한 코드 이외의 코드는 제거해도 문제가 없는지 알려주는 옵션입니다.

만약 import 한 코드를 제외하고 전부 제거해도 괜찮으려면 각 코드간에 의존성 관리에 문제가 없어야 합니다.

예를 들어 A라는 외부 패키지에서 a, b, c 라는 함수가 존재 할 때 a 함수만 import 해서 사용 할 경우

// A pacakge

export const a = () => { ... }
export const b = () => { ... }
export const c = () => { ... }
// index.js

import { a } from 'A'

a()

webpack treeShaking을 통해 bc는 삭제 될 거 라고 기대 할 수 있습니다.

하지만 만약 a 함수 내에서 bc에 의존성을 가지고 있다면 bc가 삭제 되었을 때 부작용이 발생하게 됩니다.

이러한 의존 관계를 번들링 과정에서 파싱하여 일일히 가려내기 어려우므로 이 패키지를 개발한 개발자에게 선택권을 줍니다. 번들링 과정중 import 한 코드를 제외한 나머지를 제거하더라도 문제가 되지 않는지 설정 하는 옵션이 sideEffects입니다.

sideEffectsfalse인 경우 코드를 제거해도 부작용이 없다는 뜻으로 treeShaking이 이루어지게 됩니다.

또 다른 최적화 방법

앞선 제약 조건 때문에 treeShaking을 통해 사용하지 않는 코드를 제거하는데는 한계가 있습니다.

하지만 패키지를 불러올 때 패키지의 이름만 적는 것이 아닌 패키지 내의 불러올 파일의 경로를 전부 적어주면 해당 파일의존하는 파일만 불러오게 됩니다.

  • lodash 전체를 다 불러옴
import _ from 'lodash'

_.curry()
  • currycurry에 의존하는 파일만 불러온다.
import curry from 'lodash/curry'

curry()

이렇게 사용할 경우 부분 불러오기가 가능하지만 단점도 있습니다.

  1. 외부 패키지의 내가 원하는 경로 파일 위치를 다 알아야 하고 굉장히 길어져 보기 안 좋을 수 있다.
  2. 에디터에서 자동으로 불러오기가 안되어 불편하다.
  3. 부분 임포트 해도 패키지 제작자가 작성한 방식에 따라 효과가 없을 수 도 있다.

위와 같은 단점이 있지만 1번, 2번 문제는 babel이나 webpack 설정을 통해 해결이 가능 하기도 합니다.

트랜스파일이나 번들링 시점에 import _ from 'lodash' 이렇게 작성 된 코드를 import curry from 'lodash/curry' 이렇게 바꾸어 주는 방식으로 해결 할 수 있습니다.

이것도 일부 자주 사용되는 라이브러리들은 바벨 플러그인이나 웹팩 플러그인으로 경로를 최적화 해주는 플러그인들이 npm에 배포 되어있어 잘 가져다 사용하면 됩니다.

번들링 최적화를 위해 시간을 투자한다면

번들링 최적화를 하기 위해 여러모로 삽질 해보고 실험 해본 결과는 이렇습니다.

외부 라이브러리들은 대체로 앞서 말한 제약조건 때문에 treeShaking을 통해 최적화되는 패키지들은 거의 없습니다.

굳이 안되는 treeShaking을 되게 하려는 것보다는 일부 용량이 큰 라이브러리(moment, antd)의 경우 공식 문서에 최적화 가이드를 작성해 놓은 경우가 종종 있습니다.

가이드의 지침에 따라서 웹팩이나 바벨의 설정을 따라서 해주는게 좋습니다.

여기까지 해주셨다면 다른 최적화 방안(SSR, Code splitting)에 시간 투자하는 것이 더 효과가 좋을 것입니다.

따라서 기억해야 할 중요한 사항은 다음과 같습니다.

  • 용량이 크고 꼭 사용하여야 하는 패키지는 문서에 최적화 가이드가 있는지 확인 해본다.
  • 다운 받으려는 패키지의 디펜던시가 많은지 적은지 확인한다. 적을 수록 좋다
  • https://bundlephobia.com/를 잘 활용 해보자.

긴 글 읽어 주셔서 감사합니다.


[Deok Young Kang]
Written by@[Deok Young Kang]
초보 개발자 입니다. 잘못된 내용에 대한 피드백은 언제나 환영입니다. 반갑습니다!

GitHub